package ru.ifmo.rybakov.muclumon.analysis2;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.joda.time.Seconds;

import ru.ifmo.rybakov.muclumon.DateTimeValue;
import ru.ifmo.rybakov.muclumon.NumericValue;
import ru.ifmo.rybakov.muclumon.Properties;
import ru.ifmo.rybakov.muclumon.Property;
import ru.ifmo.rybakov.muclumon.PropertyManager;
import ru.ifmo.rybakov.muclumon.PropertyType;
import ru.ifmo.rybakov.muclumon.Task;

public class TaskByBasisClusteringAlgorithm {
	
	private static class PropertyValueRange {
		public PropertyValueRange(double low, double high) {
			this.low = low;
			this.high = high;
		}
		double low;
		double high;
		
		public double getGap() {
			return Math.abs((high - low) / 100000000.0); // 5% allowed
		}
	}
	
	private static class BasisClusterDecisionStrategy implements ClusterDecisionStrategy {

		@Override
		public boolean haveToStop(double distance) {
			return distance > 0;
		}
		
	}
	
	private class BasisClusterComparator implements ClusterComparator {

		Property basis;
		
		public BasisClusterComparator(Property basis) {
			this.basis = basis;
		}
		
		@Override
		public double getDistance(Cluster first, Cluster second) {
			TaskCluster c1 = (TaskCluster) first;
			TaskCluster c2 = (TaskCluster) second;
			
			// for all pairs of tasks
			for (Task t1 : c1.getTasks()) {
				for (Task t2 : c2.getTasks()) {
					// for every regression property
					for (Property p : regressionProperties) {
						if (p.equals(basis)) {
							continue; // skip basis property
						}
						NumericValue v1 = (NumericValue) t1.getSingleValue(p);
						NumericValue v2 = (NumericValue) t2.getSingleValue(p);
						if (Math.abs(v1.getValue() - v2.getValue()) > ranges.get(p).getGap()) {
							return 1.0;
						}
					}
				}
			}
			return 0.0;
		}
	}
	
	Set<Property> regressionProperties;
	Map<Property, PropertyValueRange> ranges;
	
	public Map<Property, TaskCluster> run(TaskCluster cluster) {
		regressionProperties = cluster.getPropertySet(PropertyType.REGRESSION);
		// get ranges of every property values in the set
		ranges = new HashMap<Property, PropertyValueRange>();
		for (Property p : regressionProperties) {
			for (Task task : cluster.getTasks()) {
				NumericValue value = (NumericValue) task.getSingleValue(p);
				if (value == null) {
					throw new RuntimeException("No regression property '" + p.getName() + "' set on task (id=" + task.getId() + ",foreign_id=" + task.getForeignId());
				}
				if (ranges.containsKey(p)) { // update range
					PropertyValueRange range = ranges.get(p);
					if (range.low > value.getValue()) {
						range.low = value.getValue();
					} else if (range.high < value.getValue()) {
						range.high = value.getValue();
					}
				} else {
					ranges.put(p, new PropertyValueRange(value.getValue(), value.getValue()));
				}
			}
		}
		
		// DEBUG
		System.out.println("Ranges:");
		for (Entry<Property, PropertyValueRange> entry : ranges.entrySet()) {
			System.out.println(entry.getKey().getName() + "\t[" + entry.getValue().low + ";" + entry.getValue().high + "], gap=" + entry.getValue().getGap());
		}
		
		Map<Property, TaskCluster> result = new HashMap<Property, TaskCluster>();
		
		// now find group of properties for every property
		// build clusters for every property
		for (Property p : regressionProperties) {
			AglomerativeClusteringAlgorithm algorithm = new AglomerativeClusteringAlgorithm();
			Cluster[] basisClusters = algorithm.run(TaskCluster.toClusters(cluster.getTasks()), new BasisClusterComparator(p), new BasisClusterDecisionStrategy());
			// take one of the biggest size
			Cluster basisCluster = basisClusters[0];
			double maxTime = getMaxTime(cluster);
			double maxMetric = getBasisClusterMetric(p, (TaskCluster) basisCluster, maxTime);
			for (int i = 1; i < basisClusters.length; i++) {
				double m = getBasisClusterMetric(p, (TaskCluster) basisClusters[i], maxTime);
				System.out.println(basisClusters[i] + ":" + m);
				if (m > maxMetric) {
					maxMetric = m;
					basisCluster = basisClusters[i];
				}
			}
			
			result.put(p, (TaskCluster) basisCluster);
		}
		
		return result;
	}
	
	private double getBasisClusterMetric(Property p, TaskCluster cluster, double maxTime) {
		PropertyValueRange range = getClusterRange(p, cluster);
		PropertyValueRange wholeRange = ranges.get(p);
		// double 
		// System.out.println("RANGE: " + range.high + "-" + range.low + ";" + (range.high - range.low) / (wholeRange.high - wholeRange.low) * cluster.getTasks().size();
		
		
		return (range.high - range.low) / (wholeRange.high - wholeRange.low) * cluster.getTasks().size() * (getMaxTime(cluster) / maxTime);
	}
	
	private static PropertyValueRange getClusterRange(Property p, TaskCluster cluster) {
		Double low = null, high = null;
		for (Task task : cluster.getTasks()) {
			NumericValue value = (NumericValue) task.getSingleValue(p);
			if (low == null) {
				low = value.getValue();
				high = value.getValue();
			} else {
				if (low > value.getValue()) {
					low = value.getValue();
				} else if (high < value.getValue()) {
					high = value.getValue();
				}
			}
		}
		return new PropertyValueRange(low, high);
	}
	
	private static double getMaxTime(TaskCluster c) {
		Double max = 0.0;
		for (Task task : c.getTasks()) {
			double time = getTime(task);
			if (time > max) {
				max = time;
			}
		}
		return max;
	}
	
	public static double getTime(Task t) {
		DateTimeValue startDateTime = (DateTimeValue) t.getSingleValue(PropertyManager.get(Properties.TASK_STARTED));
		DateTimeValue finishDateTime = (DateTimeValue) t.getSingleValue(PropertyManager.get(Properties.TASK_FINISHED));
		return Seconds.secondsBetween(startDateTime.getValue(), finishDateTime.getValue()).getSeconds();
	}
	
	public Double getMax(Property p) {
		return ranges.get(p).high;
	}
}
